消息组件:弹出列表样式
本节目标
- 将弹出列表(
NoticeMessageList)与基础通知组件(Notification)拆分为独立模块 - 使用
ElTabs+ElDropdown组合实现点击弹出的消息面板 - 实现消息列表项的完整布局(头像 + 标题/标签 + 内容 + 时间)
1. 组件职责划分
Notice.vue # 业务组合组件
├── Notification.vue # 基础通知徽章(扩展 Badge)
└── NoticeMessageList.vue # 消息列表弹出层(Tabs + List + Actions)
text
关键原则:基础组件与业务功能分离。Notification 是可复用的基础组件,NoticeMessageList 是独立的列表模块,Notice 负责组合二者。
2. 创建 NoticeMessageList 组件
2.1 基础结构(ElTabs + 内容 + 操作按钮)
<!-- components/notice/NoticeMessageList.vue -->
<template>
<div class="notice-message-list" :class="wrapClass" :style="wrapStyle">
<!-- 标签页区域 -->
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane
v-for="(tab, tabIndex) in list"
:key="tabIndex"
:label="tab.title"
:name="tab.title"
>
<!-- 消息列表 -->
<ul v-if="tab.contents && tab.contents.length > 0" class="message-list">
<li
v-for="(item, index) in tab.contents"
:key="index"
class="message-item"
@click="handleClickItem(item)"
>
<!-- 消息行:头像 + 内容 -->
<el-row justify="center" align="middle">
<!-- 头像区域 -->
<el-col :span="4">
<el-avatar
v-if="item.avatar"
v-bind="Object.assign({ size: 'small' }, item.avatar)"
/>
</el-col>
<!-- 消息内容区域 -->
<el-col :span="20" class="message-content-wrap">
<!-- 标题 + 标签 -->
<el-row align="middle" class="mb-1">
<div class="message-title line-clamp-1">{{ item.title }}</div>
<el-tag
v-if="item.tag"
v-bind="item.tagProps"
size="small"
effect="dark"
class="ml-2"
>
{{ item.tag }}
</el-tag>
</el-row>
<!-- 消息内容 -->
<div v-if="item.content" class="message-desc line-clamp-2">
{{ item.content }}
</div>
<!-- 时间 -->
<div v-if="item.time" class="message-time">
{{ item.time }}
</div>
</el-col>
</el-row>
</li>
</ul>
</el-tab-pane>
</el-tabs>
<!-- 底部操作按钮 -->
<div class="action-bar">
<div
v-for="(action, index) in actions"
:key="index"
class="action-item"
@click="action.click"
>
<span v-if="action.icon" class="action-icon">
<Icon :icon="action.icon" />
</span>
<span class="action-text">{{ action.title }}</span>
</div>
</div>
</div>
</template>
vue
2.2 注意事项
- 标签页遍历:外层
v-for在el-tab-pane上遍历list(对应 Tabs),内层v-for在li上遍历tab.contents(对应每个 Tab 下的消息列表) - 条件渲染:使用
v-if判断contents、avatar、tag、content、time是否存在 - 头像属性合并:使用
Object.assign({ size: 'small' }, item.avatar)设置默认值同时保留用户自定义属性
3. 使用 ElDropdown 实现点击弹出
在 Notice.vue 中使用 ElDropdown 包裹 Notification 和 NoticeMessageList:
<!-- components/notice/Notice.vue -->
<template>
<el-dropdown trigger="click" :hide-on-click="false">
<Notification
v-bind="filteredProps"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="true" class="!p-0">
<NoticeMessageList
:list="list"
:actions="actions"
wrap-class="w-300px"
/>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Notification from './Notification.vue'
import NoticeMessageList from './NoticeMessageList.vue'
import type { NoticeProps } from './types'
const props = defineProps<NoticeProps>()
// 过滤掉 list 和 actions,只传 Notification 相关的 props
const filteredProps = computed(() => {
const { list, actions, ...rest } = props
void list
void actions
return rest
})
</script>
vue
要点:
trigger="click":点击触发弹出(而非默认的 hover):hide-on-click="false":点击列表内容时不关闭弹出层- 使用
computed过滤 Props,避免将list和actions传给Notification
4. 样式细节
4.1 消息列表样式
/* NoticeMessageList.vue scoped style */
.message-list {
@apply list-none p-0 m-0;
}
.message-item {
@apply p-3 cursor-pointer hover:bg-sky-100 transition-colors;
}
.message-title {
@apply font-medium text-sm line-clamp-1;
}
.message-desc {
@apply text-xs text-gray-500 line-clamp-2 mt-1;
}
.message-time {
@apply text-xs text-gray-400 mt-1;
}
/* 超出两行显示省略号 */
.line-clamp-1 {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
css
4.2 底部操作栏样式
.action-bar {
@apply flex items-center justify-center border-t border-gray-200;
}
.action-item {
@apply flex items-center justify-center flex-1 py-3
text-gray-500 text-sm cursor-pointer
hover:bg-sky-100 transition-colors;
}
.action-item:not(:last-child) {
@apply border-r border-gray-200;
}
.action-icon {
@apply mr-1;
}
css
经验:Element Plus 的
ElButtonGroup在需要大量样式覆盖时反而更复杂。当自定义样式需求多时,直接用div+ flex 布局手写更灵活可控。
5. 标题与标签同行布局
标题和标签需要在一行内显示,标签不被挤换行:
<el-row align="middle" class="mb-1 flex flex-nowrap!">
<div class="message-title line-clamp-1 flex-shrink min-w-0">
{{ item.title }}
</div>
<el-tag v-if="item.tag" size="small" effect="dark" class="ml-2 flex-shrink-0">
{{ item.tag }}
</el-tag>
</el-row>
html
关键:
flex-nowrap!(UnoCSS 中的!表示!important)强制不换行flex-shrink min-w-0让标题在有标签时可以收缩flex-shrink-0让标签不被压缩
6. El-Avatar 默认值处理
使用 Object.assign 合并默认值与用户传入值:
<el-avatar
v-if="item.avatar"
v-bind="Object.assign({ size: 'small' }, item.avatar)"
/>
html
这样做的好处:
- 默认
size: 'small',用户不传时自动应用 - 用户的
item.avatar中的属性可以覆盖默认值 - 比在
defineProps中设置默认值更灵活(因为这是列表渲染,无法在 Props 层面设置每项的默认值)
7. 关键知识点总结
| 知识点 | 说明 |
|---|---|
| 组件职责分离 | 基础组件(Notification)与业务列表(NoticeMessageList)独立维护 |
ElDropdown | 实现点击弹出面板,trigger="click" 控制触发方式 |
ElTabs + ElTabPane | 标签页结构,外层遍历 tabs,内层遍历 messages |
Object.assign | 合并默认属性与用户传入属性 |
line-clamp-* | CSS 多行文本截断,使用 -webkit-line-clamp |
flex-nowrap! | UnoCSS 中 ! 后缀表示 !important |
computed 过滤 Props | 从组合 Props 中提取子组件需要的部分 |
8. 下一节预告
下一节将为消息列表组件添加事件定义(点击消息项、点击头像、点击标签页切换等),并将类型定义提取到 types.d.ts 中统一管理。
↑